<template>
  <t-chat
    layout="both"
    style="height: 850px"
    :clear-history="false"
    :reverse="true"
    :text-loading="loading"
  >
    <template v-for="(item, index) in chatList" :key="index">
      <div v-if="item.show">
        <t-chat-item
          :avatar="item.avatar"
          :name="item.name"
          :role="item.role"
          :datetime="item.datetime"
          :text-loading="index === 0 && loading"
          :variant="item.variant"
          :reasoning="{
            expandIconPlacement: 'right',
            collapsePanelProps: getCollapsePanelProps(item),
          }"
        >
        </t-chat-item>
      </div>
      <div v-else>
        <t-chat-item
          :avatar="item.avatar"
          :name="item.name"
          :role="item.role"
          :datetime="item.datetime"
          :text-loading="index === 0 && loading"
          :variant="item.variant"
        >
          <template #content>
            <div v-if="item.image && item.image.startsWith('data:image')">
              <img :src="item.image" style="max-width: 100%; height: auto" />
              <t-chat-content :content="item.content"> </t-chat-content>
            </div>
            <div v-else>
              <t-chat-content :content="item.content"> </t-chat-content>
              
              <t-chat-action/>
            </div>
          </template>
        </t-chat-item>
        
        
      </div>
    </template>
    <template #footer>
      <t-chat-sender
        :stop-disabled="isStreamLoad"
        :textarea-props="{
          placeholder: '请输入消息...',
        }"
        @send="sendMessage"
        @stop="stopLoad"
      >
        <template #prefix>
          <div>
            <t-tooltip
              v-model:visible="allowToolTip"
              content="切换模型"
              trigger="hover"
            >
              <t-select
                v-model="selectValue"
                :options="selectOptions"
                value-type="object"
                @focus="allowToolTip = false"
              ></t-select>
            </t-tooltip>
          </div>
        </template>
      </t-chat-sender>
    </template>


  </t-chat>
  
</template>
<script setup>
import { ref } from "vue";

// 下拉框数据
const selectOptions = [
  {
    label: "deepseek-r1:8b",
    value: "deepseek-r1:8b",
  },
  {
    label: "deepseek-r1:14b",
    value: "deepseek-r1:14b",
  },
  {
    label: "deepseek-r1:32b",
    value: "deepseek-r1:32b",
  },
  {
    label: "qwen2.5:7b",
    value: "qwen2.5:7b",
  },
  {
    label: "llava:7b",
    value: "llava:7b",
  },
  {
    label: "llama3.2-vision:latest",
    value: "llama3.2-vision:latest",
  },
];

// 下拉框选中值
const selectValue = ref({
  label: "deepseek-r1:8b",
  value: "deepseek-r1:8b",
});

// 允许ToolTip
const allowToolTip = ref(false);

// 聊天列表，用于存储用户和助手的消息
const chatList = ref([
  {
    content: `模型由 ollama 提供数据支持`,
    role: "model-change",
    variant: "base",
  },
  {
    avatar: "https://tdesign.gtimg.com/site/chat-avatar.png",
    name: "TDesignAI",
    datetime: "今天16:38",
    show: true,
    thinking: "深度思考中...",
    reasoning: `嗯，用户问牛顿第一定律是不是适用于所有参考系。首先，我得先回忆一下牛顿第一定律的内容。牛顿第一定律，也就是惯性定律，说物体在没有外力作用时会保持静止或匀速直线运动。也就是说，保持原来的运动状态。`,
    content:
      "牛顿第一定律（惯性定律）**并不适用于所有参考系**，它只在**惯性参考系**中成立。",
    role: "assistant",
    duration: 10,
  },
]);
// 加载状态
const loading = ref(false);
// 是否流在加载中
const isStreamLoad = ref(false);
// AbortController对象用于取消SSE
const fetchAbort = ref(null);

/**
 * 发送消息功能
 * 此函数用于在聊天列表中添加用户和助手的消息
 * 它首先检查是否正在加载流数据，如果是，则不执行任何操作
 * 否则，它会创建用户和助手的消息对象，并将它们添加到聊天列表的顶部
 *
 * @param {string} value - 用户输入的消息内容
 * @param {object} context - 当前上下文，未在代码中使用，可能用于未来扩展
 */
const sendMessage = function (value, context) {
  // 检查是否正在加载流数据，如果是，则返回
  if (isStreamLoad.value) {
    return;
  }
  // 检查用户输入的消息是否为空，如果为空，则不执行任何操作
  if (!value) return;

  if (
    selectValue.value.value === "llava:7b" ||
    selectValue.value.value === "llama3.2-vision:latest"
  ) {
    // 构建用户消息对象
    const userParams = {
      avatar: "https://tdesign.gtimg.com/site/avatar.jpg",
      name: "季舟",
      datetime: new Date().toISOString(),
      content: value,
      image:
        "",
      role: "user",
      variant: "text",
    };

    // 将用户消息添加到聊天列表的顶部
    chatList.value.unshift(userParams);

    // 构建助手消息对象，内容为空，表示助手尚未回复
    const assitendParams = {
      avatar: "https://tdesign.gtimg.com/site/chat-avatar.png",
      name: "AI Vision Assistant [" + selectValue.value.value + "]",
      datetime: new Date().toISOString(),
      content: "",
      role: "assistant",
      variant: "base",
    };

    // 将助手消息添加到聊天列表的顶部
    chatList.value.unshift(assitendParams);

    handleData(userParams, true);
  } else {
    // 构建用户消息对象
    const userParams = {
      avatar: "https://tdesign.gtimg.com/site/avatar.jpg",
      name: "季舟",
      datetime: new Date().toISOString(),
      content: value,
      role: "user",
      variant: "outline",
    };

    // 将用户消息添加到聊天列表的顶部
    chatList.value.unshift(userParams);

    // 构建助手消息对象，内容为空，表示助手尚未回复
    const assitendParams = {
      avatar: "https://tdesign.gtimg.com/site/chat-avatar.png",
      name: "AI Assistant [" + selectValue.value.value + "]",
      datetime: new Date().toISOString(),
      content: "",
      role: "assistant",
      variant: "base",
    };

    // 将助手消息添加到聊天列表的顶部
    chatList.value.unshift(assitendParams);

    handleData(userParams);
  }
};

/**
 * 处理数据函数，用于向服务器发送消息并处理返回的流式数据
 * @param {string} inputValue - 用户输入的消息内容
 */
const handleData = async (userParams) => {
  const inputValue = userParams.content;
  const model = selectValue.value.value;
  // 设置加载状态为true
  loading.value = true;
  // 设置流式加载状态为true
  isStreamLoad.value = true;
  // 获取聊天列表的最后一个项
  const lastItem = chatList.value[0];

  let url = null;
  let body = null;
  if (model === "llava:7b" || model === "llama3.2-vision:latest") {
    url = "http://localhost:11434/api/generate";
    const img = userParams.image.replace("data:image/png;base64,", "");
    body = {
      model: model,
      prompt: inputValue,
      images: [img],
    };
  } else {
    url = "http://localhost:11434/api/chat";
    body = {
      messages: [
        {
          role: "user",
          content: inputValue,
        },
        {
          role: "system",
          content: "你是一个喜欢逐步解释思考过程的助手",
        },
      ],
      model: model, // 模型
      stream: true, // 流式
    };
  }

  // 调用服务器发送事件流（SSE）接口获取数据
  fetchSSE(url, body, model, {
    // 接收到消息时的处理函数
    onMessage(message) {
      console.log("message", message);
      // 设置加载状态为false
      loading.value = false;
      // 将接收到的消息内容追加到聊天列表的最后一个项中
      lastItem.content += message;
    },
    // 发生错误时的处理函数
    onError(error) {
      // 设置消息角色为错误
      lastItem.role = "error";
      // 设置消息内容为错误信息
      lastItem.content = error;
      // 设置流式加载状态为false
      isStreamLoad.value = false;
      // 设置加载状态为false
      loading.value = false;
    },
    // 数据接收完成时的处理函数
    onComplete() {
      // 设置流式加载状态为false
      isStreamLoad.value = false;
      // 设置加载状态为false
      loading.value = false;
    },
  });
};

/**
 * 通过Server-Sent Events (SSE) 流式获取数据
 *
 * @param {Array} messages 消息列表，包含聊天历史或查询信息
 * @param {Object} options 配置对象，包含事件处理函数
 * @param {Function} options.onMessage 接收到消息时的回调函数
 * @param {Function} options.onError 发生错误时的回调函数
 * @param {Function} options.onComplete 流式传输完成时的回调函数
 */
const fetchSSE = async (url, body, model, options) => {
  // 解构配置对象中的事件处理函数
  const { onMessage, onError, onComplete } = options;

  // 创建AbortController以支持取消请求
  const controller = new AbortController();
  // 获取AbortSignal用于fetch请求
  const { signal } = controller;
  // 将控制器赋值给全局或父作用域变量，以便于外部取消请求
  fetchAbort.value = controller;

  // 发起fetch请求，获取SSE响应
  const resPromise = fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
    signal,
  });

  // 处理响应
  resPromise
    .then(async (response) => {
      // console.log("response", response);

      // 检查HTTP状态码，如果非200，则调用错误处理函数并抛出错误
      if (!response.ok) {
        const { status } = response;
        onError(`HTTP error! status: ${status}`);
        throw new Error(`HTTP error! status: ${status}`);
      }

      // 获取响应体的Reader对象，用于逐块读取数据
      const reader = response.body.getReader();
      // 创建TextDecoder对象，用于解码数据块
      const decoder = new TextDecoder("utf-8");
      // 初始化缓冲区
      let buffer = "";
      // 循环读取数据块
      while (true) {
        // 读取下一数据块
        const { done, value } = await reader.read();
        // 如果done为true，则表示数据读取完毕
        if (done) {
          console.log("Stream complete");
          // 调用完成处理函数
          onComplete();
          return Promise.resolve();
        }

        // 将数据块解码为字符串并追加到缓冲区
        buffer += decoder.decode(value, { stream: true });
        // 按行分割缓冲区内容
        const lines = buffer.split("\n");
        // 更新缓冲区内容，保留未完整的一行
        buffer = lines.pop();
        // 遍历每一行数据
        for (const line of lines) {
          console.log("line", line);
          // 忽略空行
          if (line.trim() === "") continue;
          try {
            // 解析JSON数据
            const chunk = JSON.parse(line);
            // 如果数据块表示传输完成，则跳出循环
            if (chunk.done) {
              break;
            }
            const content = getContentFromChunk(chunk, model);
            // 调用消息处理函数
            onMessage(content);
          } catch (error) {}
        }
      }
    })
    .catch((error) => {
      // 错误处理
      console.log("error", error);
    });
};

const getContentFromChunk = (chunk, model) => {
  // 检查chunk是否为空或undefined，如果是则返回空字符串
  if (!chunk) {
    return "";
  }
  if (!model) {
    return "";
  }
  if (model === "llava:7b" || model === "llama3.2-vision:latest") {
    if (chunk.response) {
      return chunk.response;
    }
  } else {
    if (chunk.message && chunk.message?.content) {
      const { content } = chunk.message;
      return content;
    }
  }
  return "";
};

/**
 * 停止加载操作
 *
 * 此函数用于中断正在进行的加载过程它通过调用fetchAbort.value.abort()来终止网络请求，
 * 并将isStreamLoad.value和loading.value设置为false，以更新加载状态
 */
const stopLoad = function () {
  console.log("onStop");
  fetchAbort.value.abort();
  isStreamLoad.value = false;
  loading.value = false;
};

const getCollapsePanelProps = (item) => {
  const a = {
    header: item.thinking,
    content: item.reasoning,
  };
  return a;
};
</script>
<style lang="less">
</style>
